如何使用 C# 构建基本的 IVR(交互式语音应答)菜单系统,通过 DTMF/语音控制提升您的呼叫中心
本文详细介绍了如何以最简单的方式开发一个基本的 IVR 语音菜单系统,并解释了如何创建盲转和人工语音控制等专业的 VoIP 功能。
- 下载 basic_ivr_in_csharp_with_blind_transfer.rar - 607.7 KB
- 下载 basic_ivr_in_csharp_with_voice_control.rar
引言
在当今的商业世界中,呼叫中心占据的领域比以往任何时候都多。它们的主要任务是服务客户,这通常由销售和技术支持部门完成,例如产品支持、电话营销或市场调研。
良好的呼叫中心必须能够处理大量并发呼叫,因为对于大型公司而言,可能同时有数百个电话打入。因此,应用有效的呼叫管理选项(呼叫排队、呼叫转接、呼叫保持等)已变得必不可少。
每个运行良好的呼叫中心都有一个交互式语音应答 (IVR) 菜单,它能减轻座席的负担,因为它可以帮助客户访问与他们账户详情或可用产品/服务相关的基本服务。通常,呼叫者可以通过 IVR 访问一些简单的操作,或者您可以要求转接到人工座席。为了更好地理解,IVR 是语音菜单系统,可以将客户引导至他们需要的菜单项。它通过触摸音电话键盘输入 (DTMF 信号) 接收客户的响应。除了 DTMF 信令,IVR 系统还可以通过人工语音命令(语音控制)进行控制。在本项目中,我将解释两者的实现。
必备组件
- 我使用 C# 构建了 IVR 应用程序,因此您需要一个支持此编程语言的 IDE(集成开发环境),例如 Microsoft Visual Studio。
- 您的 PC 上还需要安装 .NET Framework。
- 由于我的 IVR 系统基于 VoIP 技术,因此您需要在 IDE 的引用中添加一些 VoIP 组件,以便以最简单的方式定义 IVR 的默认行为。由于我之前已经为其他 VoIP 开发(我创建了一个 IP PBX)使用了 Ozeki VoIP SIP SDK,因此我使用了该 SDK 预先编写的 VoIP 组件。
IDE(集成开发环境)中的设置
创建一个新项目
- 打开 Visual Studio,单击“文件”,然后单击“新建项目”。
- 选择“Visual C# 控制台应用程序”选项。
- 为您的新项目指定一个名称。
- 单击“确定”。
将 VoIP 组件添加到您的引用中,以实现并使用 SDK 的 IVR 组件。
- 右键单击“引用”。
- 选择“添加引用”选项。
- 浏览位于 SDK 安装位置的 VoIPSDK.dll 文件。
- 选择 .dll 文件,然后单击“确定”按钮。
编写代码
在我的项目中使用了 3 个类:Softphone.cs、CallHandler.cs;Program.cs。让我们一步一步地查看这 3 个类的实现。
实现 Softphone.cs 类
首先,您需要创建一个简单的软电话,它具有与普通电话相同的功能。(这是必要的,因为 IVR 需要能够接收来自其他电话的呼叫。因此,您将通过此软电话来管理传入呼叫。)`Softphone.cs` 类用于介绍如何声明、定义和初始化软电话,如何处理 VoIP SDK 的某些事件以及如何使用其函数。此类的 Program.cs 中将用于创建新的软电话。
首先,添加一些额外的 using 语句(代码示例 1):
using Ozeki.Network.Nat;
using Ozeki.VoIP;
using Ozeki.VoIP.SDK;
代码示例 1: 添加一些新的 using 语句
现在您需要从 `ISoftPhone` 和 `IPhoneLine` 接口创建软电话和电话线(**代码示例 2**)。
ISoftPhone softphone; // softphone object
IPhoneLine phoneLine; // phoneline object
代码示例 2: IsoftPhone 和 IPhoneLine 对象
在构造函数中,您还需要使用默认参数初始化此软电话(**代码示例 3**)。您需要将第一个参数表示的端口范围设置为 `minPortRange`,将最大参数设置为 `maxPortRange`。这是端口的区间。第三个参数是监听端口(5060 是 SIP 的端口)。通过订阅 `IncomingCall` 事件,可以持续监控传入呼叫。
softphone = SoftPhoneFactory.CreateSoftPhone(5000, 10000, 5060);
softphone.IncomingCall += softphone_IncomingCall;
代码示例 3: 软电话的初始化
现在您需要通过使用 Register 方法注册您的**SIP 账户**到服务器(**代码示例 4**)。此方法将注册所需的所有值作为参数:`registrationRequired`、`displayName`、`userName`、`authenticationId`、`registerPassword`、`domainHost` 和 `domainPort`。这些参数将用于通过 SIPAccount 类的构造函数创建 SIP 账户。创建账户后,该方法使用 `NatTraversalMethod` 配置网络地址转换 (NAT),以确保传入呼叫能够穿过防火墙。完成这些步骤后,系统就可以使用账户和 `NatConfiguration` 创建电话线 (`CreatePhoneLine` 方法),从而允许呼叫 IVR。最后,您需要注册此电话线。
public void Register(bool registrationRequired, string displayName, string userName, string authenticationId, string registerPassword, string domainHost, int domainPort)
{
try
{
var account = new SIPAccount(registrationRequired, displayName, userName, authenticationId, registerPassword, domainHost, domainPort);
Console.WriteLine("\n Creating SIP account {0}", account);
var natConfiguration = new NatConfiguration(NatTraversalMethod.None);
phoneLine = softphone.CreatePhoneLine(account, natConfiguration);
Console.WriteLine("Phoneline created.");
phoneLine.PhoneLineStateChanged += phoneLine_PhoneLineStateChanged;
softphone.RegisterPhoneLine(phoneLine);
}
catch(Exception ex)
{
Console.WriteLine("Error during SIP registration" + ex.ToString());
}
}
代码示例 4: SIP 账户注册
实现 CallHandler.cs 类
现在,让我们看看第二个类。`CallHandler.cs` 用于管理传入呼叫。在收到入站呼叫时,呼叫者会通过扬声器听到问候语,并在收听可选择的菜单项后,呼叫者可以通过按键盘上的按钮选择一个菜单项。
为了管理传入呼叫,您需要创建一些对象:`mediaConnector`、`audioHandler`、`phoneCallAudioSender` 和 `greetingMessageTimer`,它们分别来自 `ICall` 接口和 `MediaConnector`、`AudioHandler`、`PhoneCallAudioSender` 和 `Timer` 类(**代码示例 5**)。
ICall call;
MediaConnector mediaConnector;
AudioHandler audioHandler;
PhoneCallAudioSender phoneCallAudioSender;
Timer greetingMessageTimer;
代码示例 5: 添加一些新对象
该类的构造函数获取一个 `ICall` 类型参数,并为该类进行基本设置。它设置 `greetingMessageTimer` 的间隔(30 秒),用于在主菜单级别重复问候语,并将 `phoneCallAudioSender` 附加到呼叫(**代码示例 6**)。
public CallHandler(ICall call)
{
greetingMessageTimer = new Timer();
greetingMessageTimer.Interval = 30000;
greetingMessageTimer.Elapsed += greetingMessageTimer_Elapsed;
this.call = call;
phoneCallAudioSender = new PhoneCallAudioSender();
phoneCallAudioSender.AttachToCall(call);
mediaConnector = new MediaConnector();
}
代码示例 6: 添加一些新对象
现在来看一下方法,它们是包含一系列语句的代码块。
该类最重要的类之一是 `Start()` 方法。它将由主方法调用。在此方法中,您可以订阅 `CallStateChanged` 和 `DtmfReceived` 事件,然后接受呼叫(**代码示例 7**)。
- 呼叫状态更改事件是最重要的事件之一。它会告知服务器和客户端呼叫状态的变化。
- DTMF 接收事件会告知服务器客户端按下的按钮。它表示呼叫者想要进入另一个菜单级别。
public void Start()
{
call.CallStateChanged += call_CallStateChanged;
call.DtmfReceived += call_DtmfReceived;
call.Accept();
}
代码示例 7: 创建 Start() 方法
在 Start 方法中,您使用 `call_DtmfReceived()` 方法订阅了 `DtmfReceived` 事件,并且可以设置当呼叫者按下 DTMF 按钮时发生的情况。在**代码示例 8** 中可以看到,在此示例中,如果呼叫者按下 1,他/她就可以通过 `TextToSpeech` 类的构造函数听到一些产品信息。通过按下 2,呼叫者可以通过调用 `Mp3ToSpeaker` 方法听到一个 mp3 歌曲示例。
void call_DtmfReceived(object sender, VoIPEventArgs<DtmfInfo> e)
{
DisposeCurrentHandler();
switch (e.Item.Signal.Signal)
{
case 0: break;
case 1: TextToSpeech("Product XY has been designed for those software developers who especially interested in VoIP developments. If you prefer .NET programming languages, you might be interested in Product XY."); break;
case 2: MP3ToSpeaker(); break;
}
}
代码示例 8: 当呼叫者按下 DTMF 按钮时发生的情况
1.) TextToSpeech 方法
使用 `TextToSpeech` 方法,您可以添加您想播放给呼叫者的文本消息。它将由 `TextToSpeech` 引擎朗读。只需创建一个 `TextToSpeech` 对象,通过 Media Connector 将其连接到 `phoneCallAudioSender`,然后调用 `AddAndStartText` 方法(**代码示例 9**)。
private void TextToSpeech(string text)
{
var tts = new TextToSpeech();
audioHandler = tts;
mediaConnector.Connect(audioHandler, phoneCallAudioSender);
tts.AddAndStartText(text);
}
代码示例 9: TextToSpeech 方法
2.) MP3ToSpeaker 方法
使用 `MP3ToSpeaker` 方法的函数,IVR 可以轻松地将 MP3 文件播放给呼叫者(例如,问候语)。您只需要创建一个带有文件路径参数的 `MP3StreamPlayback` 对象,通过 `mediaConnector` 将其连接到 `PhoneCallAudioSender`,然后开始流式传输(`StartStreaming()` 方法)(**代码示例 10**)。
private void MP3ToSpeaker()
{
var mp3Player = new MP3StreamPlayback("../../test.mp3");
audioHandler = mp3Player;
mediaConnector.Connect(mp3Player, phoneCallAudioSender);
mp3Player.StartStreaming();
}
代码示例 10: MP3ToSpeaker 方法
实现 Program.cs 类
您已经到了需要实现的最后一个类。Program.cs 介绍了软电话和 callHandler 对象的使用,并处理来自呼叫者的控制台和 DTMF 事件。
首先,在 `Main` 部分,您需要创建软电话对象,以便访问 Softphone 类中创建的方法。(然后将有一些关于代码的说明以及 sipAccountInitialization 方法的调用。)您可以在此部分添加您的 SIP 账户值。现在您需要订阅 IncomingCall 事件,以便管理传入呼叫(**代码示例 11**)。
static void Main(string[] args)
{
callHandlers = new List<CallHandler>();
var softphone = new Softphone();
Console.WriteLine("/* Program usage description */");
sipAccountInitialization(softphone);
softphone.IncomigCall += softphone_IncomigCall;
Console.ReadLine();
}
代码示例 11: Main() 方法
由于 `CallHandler` 列表,此 IVR 能够管理多个呼叫。当有呼叫进来时,系统会自动接受该呼叫,然后该呼叫将被添加到列表中。每个后续的传入呼叫都将被添加到列表中。列表将一直包含这些呼叫,直到它们处于活动状态。当呼叫结束时,它将从 `CallHandler` 列表中移除。
`sipAccountInitialization()` 方法负责从用户获取 SIP 账户组件。正如您上面看到的,`SIPAccount` 构造函数需要以下值:
- 认证 ID
- 用户名(默认为认证 ID)
- 显示名称(默认为认证 ID)
- 密码
- 域主机(默认为本地主机)
- 域端口(默认为 5060)
因此,您需要为程序添加这些值,它将使用它们调用 Softphone 类的 Register 方法(**代码示例 12**)。
private static void sipAccountInitialization(Softphone softphone)
{
Console.WriteLine("Please setup your SIP account!\n");
Console.WriteLine("Please set your authentication ID: ");
var authenticationId = Read("authenticationId", true);
Console.WriteLine("Please set your user name (default:" +authenticationId +"): ");
var userName = Read("userName", false);
if (string.IsNullOrEmpty(userName))
userName = authenticationId;
Console.WriteLine("Please set your name to be displayed (default: " +authenticationId +"): ");
var displayName = Read("displayName", false);
if (string.IsNullOrEmpty(displayName))
displayName = authenticationId;
Console.WriteLine("Please set your registration password: ");
var registrationPassword = Read("registrationPassword", true);
Console.WriteLine("Please set the domain name (default: your local host): ");
var domainHost = Read("domainHost", false);
if (string.IsNullOrEmpty(domainHost))
domainHost = NetworkAddressHelper.GetLocalIP().ToString();
Console.WriteLine(domainHost);
Console.WriteLine("Please set the port number (default: 5060): ");
int domainPort;
string port = Read("domainPort", false);
if (string.IsNullOrEmpty(port))
{
domainPort = 5060;
}
else
{
domainPort = Int32.Parse(port);
}
Console.WriteLine("\nCreating SIP account and trying to register...\n");
softphone.Register(true, displayName, userName, authenticationId, registrationPassword, domainHost, domainPort);
}
代码示例 12: sipAccountInitialization() 方法
`Read()` 函数有 2 个参数:`inputname` 和 `readWhileEmpty`。如果 `readWhileEmpty` 为 false,则此组件有一个默认值(例如,域端口为 5060)。在这种情况下,输入可以为空或 null。否则,您需要为系统添加一个输入,函数将把输入返回到 `Main` 方法(**代码示例 13**)。
private static string Read(string inputName, bool readWhileEmpty)
{
while (true)
{
string input = Console.ReadLine();
if (!readWhileEmpty)
{
return input;
}
if (!string.IsNullOrEmpty(input))
{
return input;
}
Console.WriteLine(inputName +" cannot be empty!");
Console.WriteLine(inputName +": ");
}
}
代码示例 13: Read() 函数
为了在收到传入呼叫时得到通知,您需要为此目的专门设置一个事件。这就是 `IncomingCall` 事件。如果有关于等待接受的呼叫的通知,呼叫必须由 IVR 系统接受。在 `softphone_IncomingCall()` 方法中,有一个 `callHandler` 对象用于管理 `CallHandler` 类的所有方法。如果有传入呼叫,请调用 `CallHandler` 的 `Start()` 方法(**代码示例 14**)。
static void softphone_IncomigCall(object sender, Ozeki.VoIP.VoIPEventArgs<Ozeki.VoIP.IPhoneCall> e)
{
Console.WriteLine("Incoming call!");
var callHandler = new CallHandler(e.Item);
callHandler.Completed += callHandler_Completed;
lock (callHandlers)
callHandlers.Add(callHandler);
callHandler.Start();
}
代码示例 14: softphone_IncomingCall() 方法
盲转的实现
现在是时候看看如何通过盲转功能优化您的 IVR 了。
呼叫转接可以由呼叫中心服务器应用程序自动完成,也可以由人工座席协调。在盲转的情况下,第一种选择是最常见的。盲转意味着呼叫将被转接到一个随机选择的终点,基本上是第一个可用的座席。
如果用户选择此选项,系统将自动将呼叫转接到另一个电话号码。为此,您需要对此基本 IVR 代码进行一些修改。
在 Program 类的开头,创建一个静态字符串变量来存储盲转值,然后用户需要输入一个电话号码,如果他/她想使用盲转功能。如果他/她不想使用此选项,则按“0”(**代码示例 15**)。
Console.WriteLine("Please set the number for blind transferring! If you don't want to use this function of the IVR, press 0!");
blindTransfer = Read("blindTransfer", true);
代码示例 15: 创建静态字符串变量
之后,您需要将此值传递给 `CallHandler` 类。您可以通过在代码的 `softphone_IncomingCall` 部分调用一个新的(例如 `blindTransferNumber()`)方法来实现。通过这种方式,您可以将 `blindTransfer` 值传递给 `CallHandler` 类(**代码示例 16**)。
public void BlindTransferNumber(string blindTransfer)
{
blindTransferNumber = blindTransfer;
}
代码示例 16: blindTransferNumber() 方法
您唯一剩下的工作就是在 `call_DtmfReceived()` 方法的 switch 语句中添加一个新的 case。如果用户按下 3 并且他/她提供的数字在程序类中为零,则在控制台写消息表明他/她无法使用此 IVR 功能。否则,调用 `blindtransfer` 方法并将其 blindtransfer number 值作为参数传递给它(**代码示例 17**)。
case 3:
{
if (blindTransferNumber == "0")
{
TextToSpeech("You did not add any number for blind transferring!");
break;
}
else
{
call.BlindTransfer(blindTransferNumber);
break;
}
}
代码示例 17: 在 call_DtmfReceived() 方法的 switch 语句中添加一个新 case
语音控制 IVR 的实现
为了使 IVR 菜单系统更有效,您可以通过语音控制来方便呼叫者。这意味着他们可以使用人工语音命令在菜单项之间导航,而无需按下任何 DTMF 按钮。
为了实现基于人工语音的控制,您需要修改 `CallHandler` 类,并创建 `SpeechToText` 抽象类的事件和方法。
如上所示,`CallHandler.cs` 用于管理传入呼叫。为了实现语音控制功能,首先需要创建一些新对象(**代码示例 18**)。
- `PhoneCallAudioReceiver`:用于接收来自呼叫者的音频。
- `IEnumerable`:用于向系统提供词语选项,以便系统能够识别它们。
- `SpeechToText`:用于将人工语音转换为文本格式。
PhoneCallAudioReceiver phoneCallAudioReceiver;
IEnumerable<string> choices;
SpeechToText stt;
代码示例 18: 在 CallHandler.cs 类中添加一些新对象
**代码示例 19** 显示了 `CallHandler()` 构造函数,您需要在其中设置创建的对象并为其创建实例。`PhoneCallAudioReceiver` 对象应附加到呼叫。此外,您需要使用 `SpeechToText` 类的 `CreateInstance` 方法为 `SpeechToText` 对象创建实例。为了让系统能够识别呼叫者发音的语音命令,您需要向 choices 列表添加一些单词。
public CallHandler(ICall call)
{
greetingMessageTimer = new Timer();
greetingMessageTimer.Interval = 30000;
greetingMessageTimer.Elapsed += greetingMessageTimer_Elapsed;
this.call = call;
phoneCallAudioSender = new PhoneCallAudioSender();
phoneCallAudioSender.AttachToCall(call);
mediaConnector = new MediaConnector();
phoneCallAudioReceiver = new PhoneCallAudioReceiver();
phoneCallAudioReceiver.AttachToCall(call);
choices = new List<string>() { "first", "second"};
stt = SpeechToText.CreateInstance(choices);
}
代码示例 19: CallHandler() 构造函数
**代码示例 20** 中显示了 `Start()` 方法。在此部分,您需要通过 `mediaConnector` 将 `PhoneCallAudioReceiver` 连接到 stt。结果,系统将能够开始识别人工语音。您还需要订阅 `SpeechToText` 类的 `WordHypothesized` 事件。
public void Start()
{
mediaConnector.Connect(phoneCallAudioReceiver, stt);
call.CallStateChanged += call_CallStateChanged;
stt.WordHypothesized += CallHandler_WordHypothesized;
call.Accept();
}
代码示例 20: Start() 方法
当系统检测到人工语音并且识别出的单词等于 choices 列表中的一个单词时,将启动 `CallHandler_WordHypothesized()` 方法。此方法管理 switch 语句。**如下所示**,如果呼叫者说“first”,则可以通过 `TextToSpeech` 类的构造函数听到简短的产品信息。如果他/她说“second”,则呼叫者可以通过调用 `Mp3ToSpeaker` 方法听到一个 mp3 歌曲示例。
void CallHandler_WordHypothesized(object sender, SpeechDetectionEventArgs e)
{
DisposeCurrentHandler();
Console.WriteLine(e.Word.ToString());
switch (e.Word.ToString())
{
case "first": TextToSpeech("Product XY has been designed for those software developers who especially interested in VoIP developments. If you prefer .NET programming languages, you might be interested in Product XY."); break;
case "second": MP3ToSpeaker(); break;
}
}
代码示例 21: CallHandler_WordHypothesized() 方法
如何创建多级 IVR
考虑到在当今的商业世界中,更高级的多级 IVR 被广泛使用,我对我的 IVR 解决方案进行了改进。我创建了一个高级菜单系统,可以将呼叫者导航到多个菜单级别。
由于本文演示了如何构建一个基本的 IVR,我认为最好将我的改进作为技巧来呈现。如果您有兴趣开发一个多级 IVR 菜单系统,请研究我的技巧,其中逐步解释了其实现。
如何用 C# 创建多级 IVR(交互式语音应答)菜单系统: https://codeproject.org.cn/Tips/752443/How-to-create-a-multi-level-IVR-Interactive-Voice
摘要
总而言之,呼叫中心如果能够处理大量并发呼叫并拥有先进的呼叫管理功能,那么它就可以非常有效。在我的项目中,我开发了一个基本的 IVR,它可以无需人工干预就能接收和管理传入呼叫。通过构建盲转功能,我的 IVR 可以通过使用 DTMF 信令自动将呼叫者转接到实时座席。当然,您可以通过更多专业功能来扩展您的 IVR(从而改进您的呼叫中心),例如呼叫队列、语音信箱和呼叫录音等。
参考文献
理论背景
- 关于 IVR 菜单系统: https://en.wikipedia.org/wiki/Ivr
- 关于 DTMF 信令: https://en.wikipedia.org/wiki/Dtmf
- 关于呼叫转接: https://en.wikipedia.org/wiki/Call_transfer
- 关于呼叫中心: https://en.wikipedia.org/wiki/Call_center
下载必要的软件
- 下载 Microsoft Visual Studio: http://www.microsoft.com/hu-hu/download/visualstudio.aspx?q=visual+studio
- 下载 .NET Framework: http://www.microsoft.com/hu-hu/download/details.aspx?id=30653
- 下载 VoIP SIP SDK: http://voip-sip-sdk.com/p_21-download-ozeki-voip-sip-sdk-voip.html
补充信息
- IVR 开发的第一步是创建一个软电话。如果您需要有关其实现的更多信息,您可以在这里找到详细的分步指南: http://www.voip-sip-sdk.com/p_266-how-to-build-a-softphone-voip.html